import pygame
import os
from math import floor, ceil
from random import randint
from time import time

pygame.init()
pygame.mixer.init()
pygame.font.init()

WIDTH, HEIGHT = 900, 600
MouseX, MouseY = 0, 0
clock = pygame.time.Clock()
FGH = "---------------------"
key_pressed = pygame.key.get_pressed()
ScreenLeft = 0
ScreenUp = 0
ScreenRight = WIDTH
ScreenDown = HEIGHT
MaxFPS = 60
NowFPS = 60
inputting = False
command = ""
playing = True
loading = False
IFMshowwing = True
DiedCount = 0
DeltaTime = 60

# image loads
player_head_img = os.path.join("./img/player_head.png")
block_img = os.path.join("./img/stone.png")
frame_img = os.path.join("./img/frame.png")
dirt_background = os.path.join("./img/Dirt_background.png")

# sound loads
Place = [pygame.mixer.Sound(f"./audio/place/Stone_dig{i}.ogg") for i in range(1, 5)]
Break = [pygame.mixer.Sound(f"./audio/break/Stone_dig{i}.ogg") for i in range(1, 5)]
Step = [pygame.mixer.Sound(f"./audio/step/Stone_step{i}.ogg") for i in range(1, 5)]

print(FGH)
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Minecraft*  Rungame beta v1.3")
pygame.display.set_icon(pygame.image.load(player_head_img))
screen.fill("#B0C4DE")

Blocks = {}
LoadIFM = {}
BlocksRect = []
player_sprite = pygame.sprite.Group()
block_sprites = pygame.sprite.Group()

Words = pygame.font.Font("./font/Regular.otf", 13)
verson = Words.render("Minecraft  RunGame  1.3", True, (0, 0, 0))
IFMwords = ""

def ReString(string, val, pos, pos2=-1) -> str:
    if pos2 == -1: pos2 = pos
    tmp = list(string)
    for j in range(pos,pos2+1):
        tmp[j] = val[j-pos]
    return ''.join(tmp)

def RectNext(Rect1, Rect2) -> bool:
    if (
            Rect1.right == Rect2.left
            and Rect1.top < Rect2.bottom
            and Rect1.bottom > Rect2.top
    ):
        return True
    if (
            Rect1.left == Rect2.right
            and Rect1.top < Rect2.bottom
            and Rect1.bottom > Rect2.top
    ):
        return True
    if (
            Rect1.bottom == Rect2.top
            and Rect1.left < Rect2.right
            and Rect1.right > Rect2.left
    ):
        return True
    if (
            Rect1.top == Rect2.bottom
            and Rect1.left < Rect2.right
            and Rect1.right > Rect2.left
    ):
        return True
    return False


def getIFM() -> str:
    return "Screen:  left %d right %d up %d down %d\nBlock: %d %d\nX: %.2lf  Y: %.2lf\nCollider:   left %d right %d " \
           "up %d down %d\nJumping: %d\nSuspenstion: %d\nNowFPS: %d  MaxFPS: %d\nSpeed: %.2lfm/s\n" % (
                ScreenLeft,
                ScreenRight,
                ScreenUp,
                ScreenDown,
                round(player.PosX),
                round(player.PosY),
                player.PosX,
                player.PosY,
                player.ColliderLeft,
                player.ColliderRight,
                player.ColliderUp,
                player.ColliderDown,
                player.jumping,
                player.suspension,
                NowFPS,
                MaxFPS,
                player.NowSpeed
            )


def DrawText(text, color, size, pos, val=100) -> None:
    if type(text) == str:
        Texts = enumerate(text.split("\n"))
    else:
        Texts = enumerate(text)
    Word = pygame.font.Font("./font/Regular.otf", size)
    for i, line in Texts:
        screen.blit(Word.render(line, True, color), (pos[0], pos[1] + i * (size + 3)))


SoundList = []
class GSound:
    def __init__(self, Type, way):
        global SoundList
        self.type = Type
        self.way = way
        self.intensity = 64
        for i in range(len(SoundList)):
            if SoundList[i].type == self.type:
                SoundList[i].intensity = 64
                SoundList[i].way = self.way
                return
        SoundList.append(self)

    def update(self):
        self.intensity -= 1
        if self.intensity == 0:
            SoundList.remove(self)
            return


class Player(pygame.sprite.Sprite):
    def __init__(self):
        super().__init__()
        self.image = pygame.image.load(player_head_img).convert()
        self.rect = self.image.get_rect()
        self.rect.center = (WIDTH / 2, HEIGHT / 2)
        self.speed = 8
        self.y_speed = 0
        self.tick = 0
        self.ticks = floor(self.speed * 2.5)
        self.LastX = self.LastY = 0
        self.jumping = self.LastJumping = False
        self.suspension = self.LastSuspension = True
        self.resur = [9, 6]
        self.ColliderUp = 0
        self.ColliderDown = 0
        self.ColliderLeft = 0
        self.ColliderRight = 0
        self.RectX, self.RectY = (WIDTH / 2, HEIGHT / 2)
        self.PosX, self.PosY = (self.RectX / 50, self.RectY / 50)
        self.RollX, self.RollY = (self.RectX, self.RectY)
        self.ResurBool = True
        self.ResurHigh = HEIGHT / 50 + 8
        self.NowSpeed = 0

    def TouchBlock(self) -> bool:
        for block in BlocksRect:
            if self.rect.colliderect(block):
                return True
        return False

    def positioning(self) -> None:
        self.rect.centerx = self.RectX - self.RollX + WIDTH / 2
        self.rect.centery = self.RectY - self.RollY + HEIGHT / 2

    def horizontal(self, x_speed) -> None:
        self.RectX += x_speed
        self.positioning()
        while self.TouchBlock():
            if x_speed > 0:
                self.RectX -= 1
            else:
                self.RectX += 1
            self.positioning()

    def vertical(self, y_speed) -> None:
        self.RectY += round(y_speed * DeltaTime)
        self.positioning()
        while self.TouchBlock():
            if y_speed > 0:
                self.RectY -= 1
                self.jumping = False
            else:
                self.RectY += 1
            self.positioning()
            self.y_speed = 0

    def noise(self) -> None:
        global SoundWords, key_pressed
        self.tick += 1 * DeltaTime
        for block in BlocksRect:
            if RectNext(self.rect, block) and self.rect.bottom == block.top:
                if self.RectX != self.LastX:
                    if round(self.tick) == self.ticks:
                        Step[randint(0, 3)].play()
                        way = ""
                        if key_pressed[pygame.K_a]: way = "right"
                        else: way = "left"
                        GSound("STEP", way)
                        break

    def IsSuspension(self) -> bool:
        for block in BlocksRect:
            if RectNext(self.rect, block) and self.rect.bottom == block.top:
                return False
        return True

    def SetPos(self, pos_x, pos_y) -> None:
        self.PosX = pos_x
        self.PosY = pos_y
        self.RectX = self.LastX = pos_x * 50
        self.RectY = self.LastY = pos_y * 50
        self.suspension = True
        self.LastSuspension = False
        self.jumping = self.LastJumping = False
        self.tick = 0
        self.y_speed = 0

    def Resur(self) -> None:
        self.SetPos(self.resur[0], self.resur[1])

    def CompSpeed(self) -> None:
        global NowTime
        TempTime = time()
        TimeSub = TempTime - NowTime
        if TimeSub == 0:
            TimeSub = 1
        self.NowSpeed = abs(self.LastX - self.RectX) * (1 / TimeSub) / 50
        NowTime = TempTime

    def update(self):
        global DiedCount, SoundWordsm, key_pressed
        # data update
        if self.y_speed > 100:
            self.y_speed = 100
        self.suspension = self.IsSuspension()
        self.PosX = self.RectX / 50
        self.PosY = self.RectY / 50
        self.ColliderLeft = floor(self.PosX)
        self.ColliderRight = ceil(self.PosX)
        self.ColliderUp = floor(self.PosY)
        self.ColliderDown = ceil(self.PosY)
        self.speed = ceil(8 * DeltaTime)
        
        # keys check
        if not inputting:
            if key_pressed[pygame.K_a] and not key_pressed[pygame.K_d]:
                self.horizontal(-self.speed)
                self.noise()
            elif key_pressed[pygame.K_d] and not key_pressed[pygame.K_a]:
                self.horizontal(self.speed)
                self.noise()
            else:
                self.tick = self.ticks
            if round(self.tick) >= self.ticks:
                self.tick = 0

            if key_pressed[pygame.K_w]:
                if not self.suspension:
                    flag = True
                    for block in BlocksRect:
                        if RectNext(block, self.rect):
                            if self.rect.top == block.bottom:
                                flag = False
                                break
                    if flag:
                        self.y_speed = -19
                        self.jumping = True

        self.y_speed += 1 * DeltaTime
        self.vertical(self.y_speed)
        self.RollX = self.RectX
        self.RollY = self.RectY
        self.positioning()
        
        self.CompSpeed()

        if self.ResurBool:
            if self.PosY > self.ResurHigh:
                DiedCount += 1
                print("YOU DIED x%d" % DiedCount)
                self.Resur()

        if not self.suspension and self.LastSuspension:
            Step[randint(0, 3)].play()
            way = ""
            if key_pressed[pygame.K_a]:way = "right"
            elif key_pressed[pygame.K_d]:way = "left"
            else: way = "mid"
            GSound("STEP", way)

        self.LastX, self.LastY = (self.RectX, self.RectY)
        self.LastJumping = self.jumping
        self.LastSuspension = self.suspension


player: Player = Player()

player_sprite.add(player)


class Block(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(block_img).convert()
        self.XPos, self.YPos = (x, y)
        self.RectX = self.YPos * 50
        self.RectY = self.XPos * 50
        self.rect = self.image.get_rect()
        self.rect.centerx = self.XPos * 50 - player.RollX + WIDTH / 2
        self.rect.centery = self.YPos * 50 - player.RollY + HEIGHT / 2
        Blocks[(self.XPos, self.YPos)] = True
        block_sprites.add(self)
        BlocksRect.append(self.rect)

    def positioning(self) -> None:
        self.rect.centerx = self.XPos * 50 - player.RollX + WIDTH / 2
        self.rect.centery = self.YPos * 50 - player.RollY + HEIGHT / 2

    def InScreenCamere(self) -> bool:
        if self.rect.bottom < 0:
            return False
        if self.rect.top > HEIGHT:
            return False
        if self.rect.right < 0:
            return False
        if self.rect.left > WIDTH:
            return False
        return True

    def update(self):
        self.positioning()
        if Blocks[(self.XPos, self.YPos)] == 0:
            del Blocks[(self.XPos, self.YPos)]
            block_sprites.remove(self)
            BlocksRect.remove(self)
            self.kill()
            del self

def DrawBlocks():
    for block in block_sprites:
        if block.InScreenCamere():
            screen.blit(block.image, block.rect.topleft)

class Frame(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.image.load(frame_img).convert()
        self.image.set_colorkey("white")
        self.rect = self.image.get_rect()
        self.MousePosX = MouseX + player.RollX - WIDTH / 2
        self.MousePosY = MouseY + player.RollY - HEIGHT / 2
        self.PosX = floor(self.MousePosX / 50)
        self.PosY = floor(self.MousePosY / 50)

    def postioning(self) -> None:
        self.MousePosX = MouseX + player.RollX - WIDTH / 2 + 25
        self.MousePosY = MouseY + player.RollY - HEIGHT / 2 + 25
        self.PosX = floor(self.MousePosX / 50)
        self.PosY = floor(self.MousePosY / 50)
        self.rect.centerx = self.PosX * 50 - player.RollX + WIDTH / 2
        self.rect.centery = self.PosY * 50 - player.RollY + HEIGHT / 2

    def update(self):
        global inputting
        if not inputting:
            self.postioning()
        if MouseX <= 1 or MouseX >= WIDTH - 1 or MouseY <= 1 or MouseY >= HEIGHT - 1 or inputting:
            self.image.set_alpha(0)
        else:
            self.image.set_alpha(255)


def BuildBlock(XPos, YPos) -> None:
    global Blocks
    if (XPos, YPos) not in Blocks:
        Block(XPos, YPos)


def RemoveBlock(XPos, YPos) -> None:
    global Blocks
    if (XPos, YPos) in Blocks:
        Blocks[(XPos, YPos)] = False


def RunCommand(command) -> None:
    global Blocks
    if len(command) == 0:
        return
    if command[0] == "gamerule":
        rule = command[1]
        if rule == "ResurPos":
            xx = command[2]
            yy = command[3]
            if xx == '~':
                xx = player.PosX
            else:
                xx = float(xx)
            if yy == '~':
                yy = player.PosY
            else:
                yy = float(yy)
            player.resur[0] = xx
            player.resur[1] = yy
        elif rule == "PlayerSpeed":
            spd = int(command[2])
            player.speed = spd
        elif rule == "ResurCheck":
            Boolean = eval(command[2])
            player.ResurBool = Boolean
            if Boolean:
                high = command[3]
                if high == '~':
                    player.ResurHigh = player.PosY
                else:
                    player.ResurHigh = int(high)
        elif rule == "MaxFPS":
            global MaxFPS
            CheckFPS = float(command[2])
            if command[2] == "inf":
                CheckFPS = 2147483647
            else:
                CheckFPS = int(CheckFPS)
            if CheckFPS < 10:
                print("FPS too small! :(")
                return
            MaxFPS = CheckFPS
    elif command[0] == "setblock":
        block = command[3]
        xx = command[1]
        yy = command[2]
        if xx == '~':
            xx = round(player.PosX)
        else:
            xx = int(xx)
        if yy == '~':
            yy = round(player.PosY)
        else:
            yy = int(yy)
        if block == "stone":
            BuildBlock(xx, yy)
        elif block == "air":
            RemoveBlock(xx, yy)
    elif command[0] == "fill":
        try:
            x1 = command[1]
            y1 = command[2]
            x2 = command[3]
            y2 = command[4]
        except:
            print("Missing parameters :(")
        try:
            if x1 == '~':
                x1 = round(player.PosX)
            else:
                x1 = int(x1)
            if y1 == '~':
                y1 = round(player.PosY)
            else:
                y1 = int(y1)
            if x2 == '~':
                x2 = round(player.PosX)
            else:
                x2 = int(x2)
            if y2 == '~':
                y2 = round(player.PosY)
            else:
                y2 = int(y2)
        except:
            print("Wrong position :(")
        if x1 > x2:
            tmp = x1
            x1 = x2
            x2 = tmp
        if y1 > y2:
            tmp = y1
            y1 = y2
            y2 = tmp
        block = command[5]
        for i in range(x1, x2 + 1):
            for j in range(y1, y2 + 1):
                if block == "stone":
                    BuildBlock(i, j)
                elif block == "air":
                    RemoveBlock(i, j)
        print("Fill block Successful")
    elif command[0] == "kill":
        player.Resur()
    elif command[0] == "tp":
        try:
            xx = float(command[1])
            yy = float(command[2])
            player.SetPos(xx, yy)
        except:
            print("Wrong position :(")
    elif command[0] == "save":
        global DiedCount
        try:
            MapName = command[1]
            MapFile = open(MapName + ".rdb", "w")
            MapFile.write("{")
            for (xx, yy) in Blocks:
                MapFile.write("(%d,%d):True," % (xx, yy))
            MapFile.write("}\n")
            MapFile.write("(%d,%d)\n" % (player.PosX, player.PosY))
            MapFile.write("%d\n" % DiedCount)
            print("Save world Successful :)")
        except:
            print("Faild to save world :(")
    elif command[0] == "loadmap":
        global LoadBlocks, loading, playing
        MapName = command[1]
        try:
            if not os.path.exists(MapName + ".rdb"):
                print("File " + MapName + ".rdb not found :(")
                return
            MapFile = open(MapName + ".rdb", "r")
            LoadIFM["Blocks"] = eval("".join(MapFile.readline()))
            print("Map blocks are ready")
            LoadIFM["PlayerPos"] = eval("".join(MapFile.readline()))
            LoadIFM["DiedCount"] = eval("".join(MapFile.readline()))
            print("Map informations are ready")
            loading = True
            print("Wait to load world...")
        except:
            print("Failed to load other world informations :(")
    elif command[0] == "exit" or command[0] == "quit":
        playing = False
    else:
        print("No command named \"{}\" :(".format(command[0]))

Inputtick = 0
Inputthing = ''
def InputCommand(events) -> None:
    global key_pressed, command, Inputtick, Inputthing
    for event in events:
        if event.type == pygame.KEYDOWN:
            if key_pressed[pygame.K_BACKSPACE] and command:
                command = command[:-1]
            else:
                Inputthing = event.unicode
                command += Inputthing


RunCommand(["fill", "0", "11", "18", "15", "stone"])
FrameGroup = pygame.sprite.Group()
frame = Frame()
FrameGroup.add(frame)
dirt_bg = pygame.image.load(dirt_background).convert()
TempFlag = False
NowTime = time()

while playing:
    DeltaTime = clock.tick(MaxFPS) / 1000.0 * 60
    NowFPS = clock.get_fps()
    KeyDown = False
    MouseX, MouseY = pygame.mouse.get_pos()
    ScreenLeft = player.RectX - WIDTH/2
    ScreenRight = player.RectX + WIDTH/2
    ScreenUp = player.RectY - HEIGHT/2
    ScreenDown = player.RectY + HEIGHT/2
    events = pygame.event.get()
    key_pressed = pygame.key.get_pressed()
    for event in events:
        if event.type == pygame.QUIT:
            playing = False
            break
        elif event.type == pygame.MOUSEBUTTONDOWN:
            MousePressed = pygame.mouse.get_pressed()
            if 0 <= MouseX <= WIDTH and 0 <= MouseY <= HEIGHT and not inputting:
                PosX, PosY = (frame.PosX, frame.PosY)
                way = ""
                if PosX > player.PosX:way = "right"
                elif PosX < player.PosX:way = "left"
                else: way = "mid"
                if (PosX, PosY) not in Blocks and MousePressed[2]:
                    if not (
                            (PosX == player.ColliderLeft and PosY == player.ColliderUp)
                            or (PosX == player.ColliderRight and PosY == player.ColliderUp)
                            or (PosX == player.ColliderLeft and PosY == player.ColliderDown)
                            or (PosX == player.ColliderRight and PosY == player.ColliderDown)
                    ):
                        BuildBlock(PosX, PosY)
                        Place[randint(0, 3)].play()
                        GSound("PLACE", way)
                elif (PosX, PosY) in Blocks and MousePressed[0]:
                    RemoveBlock(PosX, PosY)
                    Break[randint(0, 3)].play()
                    GSound("BREAK", way)
        elif event.type == pygame.KEYDOWN:
            if key_pressed[pygame.K_F3]:
                IFMshowwing = not IFMshowwing
            if key_pressed[pygame.K_SLASH]:
                inputting = True
                command = ""
            if key_pressed[pygame.K_ESCAPE]:
                inputting = False
                command = ""
            if key_pressed[pygame.K_t] and not inputting:
                inputting = True
                command = ""
                TempFlag = True
            if key_pressed[pygame.K_RETURN]:
                if inputting:
                    inputting = False
                    if command[0] == '/':
                        command = command[1:]
                        RunCommand(command.split())
                    command = ""

    screen.fill("#B0C4DE")

    if loading:
        screen.blit(dirt_bg, (0, 0))
        try:
            block_sprites.empty()
            BlocksRect = []
            Blocks = {}
            for block in LoadIFM["Blocks"]:
                Block(block[0], block[1])
            player.SetPos(LoadIFM["PlayerPos"][0], LoadIFM["PlayerPos"][1])
            DiedCount = LoadIFM["DiedCount"]
            print("Load map Successful :)")
        except:
            print("Failed to load world :(")
        loading = False
        LoadIFM = {}
    if not loading:
        player_sprite.update()
        block_sprites.update()
        FrameGroup.update()
        player_sprite.draw(screen)
        DrawBlocks()
        FrameGroup.draw(screen)
        SoundLength = len(SoundList)
        for i in range(SoundLength):
            GSoundWord = SoundList[SoundLength-i-1]
            SoundWord = GSoundWord.type.center(20)
            if GSoundWord.way == "left":
                SoundWord = ReString(SoundWord, "<-", 2, 3)
            elif GSoundWord.way == "right":
                SoundWord = ReString(SoundWord, "->", 14, 15)
            DrawText(SoundWord, "black", 13, (780, HEIGHT-20-len(SoundList)*18 + i*18))
            SoundList[SoundLength-i-1].update()
        if IFMshowwing:
            IFMwords = getIFM()
            DrawText(IFMwords, "black", 16, (10, 10))

    if inputting:
        InputCommand(events)
        if TempFlag:
            command = ""
            TempFlag = False
        DrawText(command, 'black', 13, (30, 550))
    pygame.display.update()

pygame.quit()
